﻿/*
   Copyright 2012 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Security.AccessControl;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Flower.Client;
using Flower.Directory.Host.Default;
using Flower.Directory.Management;
using Flower.Directory.Util;
using Flower.Services;
using Flower.Services.Data;
using NUnit.Framework;
using Flower.Workflow;
using Flower.Logging;
using Flower.Directory.Host;
using System.Collections.Concurrent;
using Flower.Testing;

namespace Flower.Client.Tests
{
    [TestFixture]
    public class FlowerClientTests
    {
        private static readonly Log Log = LogManager.CreateLog(typeof(FlowerClientTests).Name);
        private static readonly IScriptEnginePool ScriptEnginePool = new ScriptEnginePool(10);

        #region Create directory

        public IDirectory CreateDirectory()
        {
            var proxyProvider = new DirectoryProxyProvider();
            var processesDispatcher = new ProcessesDispatcher();
            var dir = proxyProvider.MakeProxy(new MemoryDirectory.Directory(ScriptEnginePool, proxyProvider, processesDispatcher));

            return dir;
        }

        #endregion

        #region Lock/Unlock

        [Test]
        public void LockUnlockTests()
        {
            var dir = CreateDirectory();

            new DirectoryBuilder(dir)
                .Root("/Sets/Shared")
                    .Set("Resource1", typeof(int).FullName, 1)
                        .Folder("Root").End()
                    .End()
                    .Set("Resource2", typeof(int).FullName, 1)
                        .Folder("Root").End()
                    .End()
                .End();

            const string RES1 = "/Sets/Shared/Resource1/Root";
            const string RES2 = "/Sets/Shared/Resource2/Root";

            using (var client = new FlowerClient(dir))
            {
                LockFailure[] lockFailures = client.Lock(new[] { RES1, RES2 }, "Owner1");

                Assert.IsTrue(lockFailures.Length == 0);
                Assert.IsTrue(dir.Exists(RES1 + "/Owner1", DateTime.MinValue));
                Assert.IsTrue(dir.Exists(RES2 + "/Owner1", DateTime.MinValue));

                lockFailures = client.Lock(new[] { RES1, RES2 }, "Owner1");

                Assert.IsTrue(lockFailures.Length == 0);
                Assert.IsTrue(dir.Exists(RES1 + "/Owner1", DateTime.MinValue));
                Assert.IsTrue(dir.Exists(RES2 + "/Owner1", DateTime.MinValue));

                client.Unlock(new[] { RES1, RES2 }, "Owner1");

                Assert.IsTrue(lockFailures.Length == 0);
                Assert.IsTrue(dir.Exists(RES1 + "/Owner1", DateTime.MinValue));
                Assert.IsTrue(dir.Exists(RES2 + "/Owner1", DateTime.MinValue));

                client.Unlock(new[] { RES2 }, "Owner1");

                Assert.IsTrue(!dir.Exists(RES2 + "/Owner1", DateTime.MinValue));
            }
        }

        #endregion

        #region Put/Remove message

        [Test]
        public void PutRemoveMessage()
        {
            var dir = CreateDirectory();

            new DirectoryBuilder(dir)
                .Root("/Sets/Shared")
                    .Set("Set0", typeof(int).FullName, 0)
                    .End()
                    .Set("Set1", typeof(int).FullName, 1)
                    .End()
                .End();

            using (var client = new FlowerClient(dir))
            {
                Assert.IsTrue(client.PutMessage("/Sets/Shared/Set1/Subset", "1", 1, BlobFormat.BinXml));
                Assert.AreEqual(1, client.Directory.CountChildren("/Sets/Shared/Set1/Subset", DirectoryEntryTypes.Message));
                
                client.RemoveMessage("/Sets/Shared/Set1/Subset/1");
                Assert.AreEqual(0, client.Directory.CountChildren("/Sets/Shared/Set1/Subset", DirectoryEntryTypes.Message));

                Assert.IsTrue(client.PutMessage("/Sets/Shared/Set1/Subset", "1", 1, BlobFormat.BinXml));
                Assert.AreEqual(1, client.Directory.CountChildren("/Sets/Shared/Set1/Subset", DirectoryEntryTypes.Message));
                
                client.RemoveMessage("/Sets/Shared/Set1/Subset/1");
                Assert.AreEqual(0, client.Directory.CountChildren("/Sets/Shared/Set1/Subset", DirectoryEntryTypes.Message));

                Assert.AreEqual(3, client.PutMessages("/Sets/Shared/Set0/Subset", new[] { "1", "2", "3" }, new[] { 1, 2, 3 }, BlobFormat.BinXml));
                Assert.AreEqual(3, client.Directory.CountChildren("/Sets/Shared/Set0/Subset", DirectoryEntryTypes.Message));

                Assert.AreEqual(3, client.PutMessages("/Sets/Shared/Set0/Subset", null, new[] { 1, 2, 3 }, BlobFormat.BinXml));
                Assert.AreEqual(6, client.Directory.CountChildren("/Sets/Shared/Set0/Subset", DirectoryEntryTypes.Message));
            }
        }

        #endregion

        #region Services

        public class ServiceData
        {
            public int Value { get; set; }
        }

        public interface IService
        {
            ServiceData GetData();
        }

        public class Service : IService
        {
            private readonly ServiceData _serviceData;

            public Service(ServiceData serviceData)
            {
                _serviceData = serviceData;
            }

            public ServiceData  GetData()
            {
 	            return _serviceData;
            }
        }

        private static readonly string GLOBAL_SERVICE_CONTAINER1 =
            @"<objects xmlns=""http://www.springframework.net"">" +
            @"  <object id=""ServiceData1"" type=""" + typeof(ServiceData).FullName + @""">" +
            @"    <property name=""Value"" value=""1"" />" +
            @"  </object>" +
            @"  <object id=""ServiceData2"" type=""" + typeof(ServiceData).FullName + @""">" +
            @"    <property name=""Value"" value=""2"" />" +
            @"  </object>" +
            @"</objects>";
        private static readonly string GLOBAL_SERVICE_CONTAINER2 =
            @"<objects xmlns=""http://www.springframework.net"">" +
            @"  <object id=""ServiceData1"" type=""" + typeof(ServiceData).FullName + @""">" +
            @"    <property name=""Value"" value=""2"" />" +
            @"  </object>" +
            @"  <object id=""ServiceData2"" type=""" + typeof(ServiceData).FullName + @""">" +
            @"    <property name=""Value"" value=""2"" />" +
            @"  </object>" +
            @"</objects>";
        private static readonly string SERVICE_CONTAINER1 =
            @"<objects xmlns=""http://www.springframework.net"">" +
            @"  <object id=""Service1"" type=""" + typeof(Service).FullName + @""">" +
            @"    <constructor-arg ref=""ServiceData1""/>" +
            @"  </object>" +
            @"  <object id=""Service2"" type=""" + typeof(Service).FullName + @""">" +
            @"    <constructor-arg ref=""ServiceData2""/>" +
            @"  </object>" +
            @"</objects>";
        private static readonly string SERVICE_CONTAINER2 =
            @"<objects xmlns=""http://www.springframework.net"">" +
            @"  <object id=""Service1"" type=""" + typeof(Service).FullName + @""">" +
            @"    <constructor-arg ref=""ServiceData1""/>" +
            @"  </object>" +
            @"  <object id=""Service2"" type=""" + typeof(Service).FullName + @""">" +
            @"    <constructor-arg ref=""ServiceData1""/>" +
            @"  </object>" +
            @"</objects>";
        private static readonly string PROCESSOR_SERVICE_CONTAINER1 =
            @"<objects xmlns=""http://www.springframework.net"">" +
            @"  <object id=""ServiceData1"" type=""" + typeof(ServiceData).FullName + @""">" +
            @"    <property name=""Value"" value=""1"" />" +
            @"  </object>" +
            @"  <object id=""ServiceData2"" type=""" + typeof(ServiceData).FullName + @""">" +
            @"    <property name=""Value"" value=""2"" />" +
            @"  </object>" +
            @"  <object id=""Service1"" type=""" + typeof(Service).FullName + @""">" +
            @"    <constructor-arg ref=""ServiceData1""/>" +
            @"  </object>" +
            @"  <object id=""Service2"" type=""" + typeof(Service).FullName + @""">" +
            @"    <constructor-arg ref=""ServiceData2""/>" +
            @"  </object>" +
            @"</objects>";
        private static readonly string PROCESSOR_SERVICE_CONTAINER2 =
            @"<objects xmlns=""http://www.springframework.net"">" +
            @"  <object id=""ServiceData1"" type=""" + typeof(ServiceData).FullName + @""">" +
            @"    <property name=""Value"" value=""1"" />" +
            @"  </object>" +
            @"  <object id=""ServiceData2"" type=""" + typeof(ServiceData).FullName + @""">" +
            @"    <property name=""Value"" value=""2"" />" +
            @"  </object>" +
            @"  <object id=""Service1"" type=""" + typeof(Service).FullName + @""">" +
            @"    <constructor-arg ref=""ServiceData1""/>" +
            @"  </object>" +
            @"  <object id=""Service2"" type=""" + typeof(Service).FullName + @""">" +
            @"    <constructor-arg ref=""ServiceData1""/>" +
            @"  </object>" +
            @"</objects>";

        private class ServicesInjectionTarget
        {
            [Service("/Services/Test/Service1")]
            private IService _service = null;
            public IService ServiceField { get { return _service; } }

            [Service("/Services/Test/Service2")]
            public IService Service { get; set; }

            public IDirectory DirectoryField = null;
            public IDirectory Directory { get; set; }
        }

        [Test]
        public void GetService()
        {
            var dir = CreateDirectory();

            new DirectoryBuilder(dir)
                .Root("/Services")
                    .Folder("Flower")
                        .ServiceContainer("Global", GLOBAL_SERVICE_CONTAINER1).End()
                    .End()
                    .ServiceContainer("Test", SERVICE_CONTAINER1).End()
                .End();

            var cliSettings = new FlowerClientSettings 
            {
                ApplicationContextExpirationCheckInterval = TimeSpan.FromSeconds(1)
            };

            using (var cli = new FlowerClient(dir, cliSettings))
            {
                var svc = cli.GetService<Service>("/Services/Test/Service1");
                Assert.AreEqual(1, svc.GetData().Value);

                svc = cli.GetService<Service>("/Services/Test/Service2");
                Assert.AreEqual(2, svc.GetData().Value);

                var xml = new XmlDocument();
                xml.LoadXml(SERVICE_CONTAINER2);
                var svcContainer = (ServiceContainer)dir.Get("/Services/Test");
                svcContainer.Configuration.Data =
                    BlobUtil.WriteBlob(BlobFormat.TextXml, xml.WriteTo).Data;
                dir.Save("/Services", svcContainer);

                Thread.Sleep(3000);

                svc = cli.GetService<Service>("/Services/Test/Service2");
                Assert.AreEqual(1, svc.GetData().Value);

                xml.LoadXml(GLOBAL_SERVICE_CONTAINER2);
                svcContainer = (ServiceContainer)dir.Get("/Services/Flower/Global");
                svcContainer.Configuration.Data =
                    BlobUtil.WriteBlob(BlobFormat.TextXml, xml.WriteTo).Data;
                dir.Save("/Services/Flower", svcContainer);

                Thread.Sleep(3000);

                svc = cli.GetService<Service>("/Services/Test/Service1");
                Assert.AreEqual(2, svc.GetData().Value);
            }
        }

        [Test]
        public void GetServiceAutorefresh()
        {
            var dir = CreateDirectory();

            new DirectoryBuilder(dir)
                .Root("/Services")
                    .Folder("Flower")
                        .ServiceContainer("Global", GLOBAL_SERVICE_CONTAINER1).End()
                    .End()
                    .ServiceContainer("Test", SERVICE_CONTAINER1).End()
                .End();

            var cliSettings = new FlowerClientSettings
            {
                ApplicationContextExpirationCheckInterval = TimeSpan.FromSeconds(1)
            };

            using (var cli = new FlowerClient(dir, cliSettings))
            {
                var svc1 = cli.GetService<IService>("/Services/Test/Service1");
                Assert.AreEqual(1, svc1.GetData().Value);

                var svc2 = cli.GetService<IService>("/Services/Test/Service2");
                Assert.AreEqual(2, svc2.GetData().Value);

                var xml = new XmlDocument();
                xml.LoadXml(SERVICE_CONTAINER2);
                var svcContainer = (ServiceContainer)dir.Get("/Services/Test");
                svcContainer.Configuration.Data =
                    BlobUtil.WriteBlob(BlobFormat.TextXml, xml.WriteTo).Data;
                dir.Save("/Services", svcContainer);

                Thread.Sleep(3000);

                Assert.AreEqual(1, svc2.GetData().Value);

                xml.LoadXml(GLOBAL_SERVICE_CONTAINER2);
                svcContainer = (ServiceContainer)dir.Get("/Services/Flower/Global");
                svcContainer.Configuration.Data =
                    BlobUtil.WriteBlob(BlobFormat.TextXml, xml.WriteTo).Data;
                dir.Save("/Services/Flower", svcContainer);

                Thread.Sleep(3000);

                Assert.AreEqual(2, svc1.GetData().Value);
            }
        }

        [Test]
        public void ServicesInjection()
        {
            var dir = CreateDirectory();

            new DirectoryBuilder(dir)
                .Root("/Services")
                    .Folder("Flower")
                        .ServiceContainer("Global", GLOBAL_SERVICE_CONTAINER1).End()
                    .End()
                    .ServiceContainer("Test", SERVICE_CONTAINER1).End()
                .End();

            var cliSettings = new FlowerClientSettings();

            using (var cli = new FlowerClient(dir, cliSettings))
            {
                var target = new ServicesInjectionTarget();

                cli.InjectServices(target);

                Assert.IsNotNull(target.Service);
                Assert.IsNotNull(target.ServiceField);
                Assert.IsNotNull(target.Directory);
                Assert.IsNotNull(target.DirectoryField);
                Assert.AreEqual(dir, target.Directory);
                Assert.AreEqual(dir, target.DirectoryField);
            }
        }

        [Test]
        public void GetProcessorService()
        {
            var dir = CreateDirectory();

            new DirectoryBuilder(dir)
                .Root("/Processors")
                    .Processor("TestProcessor", PROCESSOR_SERVICE_CONTAINER1).End()
                .End();

            var cliSettings = new FlowerClientSettings
            {
                ApplicationContextExpirationCheckInterval = TimeSpan.FromSeconds(3)
            };

            using (var cli = new FlowerClient(dir, cliSettings))
            {
                var svc = cli.GetService<Service>("/Processors/TestProcessor/Service1");
                Assert.AreEqual(1, svc.GetData().Value);

                svc = cli.GetService<Service>("/Processors/TestProcessor/Service2");
                Assert.AreEqual(2, svc.GetData().Value);

                var xml = new XmlDocument();
                xml.LoadXml(PROCESSOR_SERVICE_CONTAINER2);
                var proc = (Processor)dir.Get("/Processors/TestProcessor");
                proc.Configuration.Data =
                    BlobUtil.WriteBlob(BlobFormat.TextXml, xml.WriteTo).Data;
                dir.Save("/Processors", proc);

                Thread.Sleep(7000);

                svc = cli.GetService<Service>("/Processors/TestProcessor/Service2");
                Assert.AreEqual(1, svc.GetData().Value);
            }
        }

        [Test]
        public void GetServiceParallel()
        {
            var dir = CreateDirectory();

            new DirectoryBuilder(dir)
                .Root("/Services")
                    .Folder("Flower")
                        .ServiceContainer("Global", GLOBAL_SERVICE_CONTAINER1).End()
                    .End()
                    .ServiceContainer("Test", SERVICE_CONTAINER1).End()
                .End();

            using (var cli = new FlowerClient(dir))
            {
                var loadings = new List<Task<Service>>();
                var startedEvents = new List<ManualResetEventSlim>();
                var goEvent = new ManualResetEventSlim();

                for (int i = 0; i < 3; i++)
                {
                    var startedEvent = new ManualResetEventSlim();

                    startedEvents.Add(startedEvent);

                    loadings.Add
                    (
                        Task.Factory.StartNew
                        (
                            () => 
                            {
                                startedEvent.Set();
                                goEvent.Wait();
                                return cli.GetService<Service>("/Services/Test/Service1");
                            }
                        )
                    );
                }

                foreach (var ev in startedEvents)
                {
                    ev.Wait();
                }

                goEvent.Set();

                foreach (var loading in loadings)
                {
                    if (!loading.Wait(TimeSpan.FromSeconds(10)))
                    {
                        Assert.Fail();
                    }
                }
            }
        }
        #endregion

        #region Processes

        [DataContract(Namespace = "test-ns")]
        public class Workflow : Flower.Workflow.Process, IWorkflow<int, int>
        {
            public void DefineLocalSets(ISetsBuilder bld)
            {
                bld
                    .Set("Set1", typeof(int), 1)
                    .Set("Set2", typeof(string));
            }

            public int Initialize(IInitializationContext context, int arg)
            {
                return arg + 1;
            }

            public void DefineProcess(IProcessBuilder bld)
            {
            }
        }

        [Test]
        public void StartProcess()
        {
            var dir = CreateDirectory();

            new DirectoryBuilder(dir)
                .Root("/Workflows")
                    .Workflow("Test", typeof(Workflow).AssemblyQualifiedName, null).End()
                    .Workflow("TestWithPreferred", typeof(Workflow).AssemblyQualifiedName, new[] { "Proc1" }).End()
                .End()
                .RegisterProcessor("Proc1")
                .RegisterProcessor("Proc2")
                .RegisterProcessor("Proc3");

            using (var cli = new FlowerClient(dir))
            {
                const int procsCount = 10;

                var allPids = new List<string>();

                for (int i = 0; i < procsCount; i++)
                {
                    var startResult =
                        cli.StartProcess<int, int>("/Workflows/Test", i, Log);

                    Assert.IsTrue(startResult.Pid != null);
                    Assert.IsTrue(startResult.Result == i + 1);

                    Assert.IsTrue(dir.Exists(string.Format("/Sets/Local/{0}/Set1", startResult.Pid), DateTime.MinValue));
                    Assert.IsTrue(dir.Exists(string.Format("/Sets/Local/{0}/Set2", startResult.Pid), DateTime.MinValue));
                    Assert.IsTrue(dir.Exists(string.Format("/Sets/Local/{0}/Log", startResult.Pid), DateTime.MinValue));

                    allPids.Add(startResult.Pid);

                    dir.Move
                    (
                        startResult.Pid,
                        string.Concat("/Processors/", startResult.ProcessorName, "/Running")
                    );
                }

                var createdProcesses =
                    dir.Find("/Processors/**", DirectoryEntryTypes.Process);

                Assert.IsTrue(createdProcesses.Select(p => p.Id).Intersect(allPids).Count() == allPids.Count);

                var procsCountPerProcessor = new List<long>();
                foreach (DirectoryEntry pendingFolder in dir.Find("/Processors/*/Running", DirectoryEntryTypes.Folder))
                {
                    procsCountPerProcessor.Add
                    (
                        dir.CountChildren(pendingFolder.Id, DirectoryEntryTypes.Process)
                    );
                }

                Assert.IsTrue(procsCountPerProcessor.Count == 3);

                for (int i = 0; i < procsCountPerProcessor.Count - 1; i++)
                {
                    Assert.IsTrue(procsCountPerProcessor[i] > 0);

                    for (int j = i + 1; j < procsCountPerProcessor.Count; j++)
                    {
                        Assert.IsTrue(Math.Abs(procsCountPerProcessor[i] - procsCountPerProcessor[j]) < 2);
                    }
                }

                long proc1ProcessesCountBefore = 
                    dir.CountChildren("/Processors/Proc1/Running", DirectoryEntryTypes.Process);

                for (int i = 0; i < procsCount; i++)
                {
                    var startResult =
                        cli.StartProcess<int, int>("/Workflows/TestWithPreferred", i, Log);

                    Assert.IsTrue(startResult.Pid != null);
                    Assert.IsTrue(startResult.Result == i + 1);

                    dir.Move
                    (
                        startResult.Pid,
                        string.Concat("/Processors/", startResult.ProcessorName, "/Running")
                    );
                }

                long proc1ProcessesCountAfter =
                    dir.CountChildren("/Processors/Proc1/Running", DirectoryEntryTypes.Process);

                Assert.AreEqual(proc1ProcessesCountBefore + procsCount, proc1ProcessesCountAfter);
            }
        }

        #endregion

        #region Assembly resolution

        [Test]
        public void AssemblyResolution()
        {
            const string basePathDebug = @"..\..\..\Sample.Workflows\bin\Debug";
            const string basePathRelease = @"..\..\..\Sample.Workflows\bin\Release";

            var dir = new MemoryDirectory.Directory(new ScriptEnginePool(1));

            if (File.Exists(basePathDebug + @"\Sample.Workflows.dll"))
            {
                dir.ImportAssemblies
                (
                    new[] 
                    {
                        basePathDebug + @"\Sample.Workflows.dll",
                        basePathDebug + @"\Sample.DataContracts.dll",
                        basePathDebug + @"\Sample.ServiceContracts.dll"
                    }
                );
            }
            else
            {
                dir.ImportAssemblies
                (
                    new[] 
                    {
                        basePathRelease + @"\Sample.Workflows.dll",
                        basePathRelease + @"\Sample.DataContracts.dll",
                        basePathRelease + @"\Sample.ServiceContracts.dll"
                    }
                );
            }
            
            using (var cli = new FlowerClient(dir))
            {
                Assert.IsNotNull(Type.GetType("Sample.Workflows.BubbleSort, Sample.Workflows"));
            }
        }

        #endregion
    }
}
